﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Service;
using XcoAppSpaces.Core;
using XcoAppSpaces.Contracts.Logging;
using Microsoft.Ccr.Core;
using System.Threading;
using XcoAppSpaces.Core.Communication;

namespace XcoAppSpaces.Discovery
{
	public class XcoDiscoveryService : IXcoDiscoveryService
	{
		private readonly IXcoLogger log = XcoLoggerFactory.CreateInstance(typeof(XcoDiscoveryService));

		private IXcoAppSpace space = null;
		private PDiscoveryWorker discoveryWorker = null;
		private readonly Dictionary<string, Port<DiscoveryRequest>> registeredSpaces = new Dictionary<string, Port<DiscoveryRequest>>();
		private readonly Port<DiscoveryRequest> localDiscoveryPort = new Port<DiscoveryRequest>();

        private Timer discoveryWorkerCheckTimer;
        private const int discoveryWorkerCheckTimerInterval = 10000;
		
		public XcoDiscoveryService() : this(null)
		{
		}

		public XcoDiscoveryService(string serverAddress)
		{
			this.ServerAddress = serverAddress;
			this.IsServer = (serverAddress == null);
		}

		#region IXcoDiscoveryService Members

		public bool IsServer { get; set; }
		public string ServerAddress { get; set; }

		public DiscoveryResponse Resolve<T>(string workerName)
			where T : IPort
		{
			log.Debug("Processing local discovery request for Worker: Type=" + typeof(T) + (workerName != null ? (", Name=" + workerName) : ""));
			
			if (space.Resolve<T>(workerName) != null)
				return new DiscoveryResponse { Result = DiscoveryResult.WorkerFoundLocal };
			CheckIfDiscoveryWorkerIsAvailable();
			return PostDiscoveryRequestAndWaitForResult(typeof(T), workerName);
		}

		private void CheckIfDiscoveryWorkerIsAvailable()
		{
			TryConnectToDiscoveryWorker();
			if (discoveryWorker == null)
				throw new XcoDiscoveryException("Discovery Server at " + this.ServerAddress + " could not be reached!");
		}

		private DiscoveryResponse PostDiscoveryRequestAndWaitForResult(Type workerType, string workerName)
		{
			try
			{
				var responsePort = new Port<DiscoveryResponse>();
				discoveryWorker.Send(new DiscoveryRequest { WorkerType = workerType, WorkerName = workerName, ResponsePort = responsePort/*, LocalSpaceAddress = space.Address*/ });
				var result = responsePort.TestSync(20000);
				return result;
			}
			catch (Exception ex)
			{
				throw new XcoDiscoveryException("Discovery Server at " + this.ServerAddress + " could not be reached!", ex);
			}
		}

		public void Register(string spaceAddress, Port<DiscoveryRequest> discoveryPort)
		{
			log.Debug("Space registered at discovery service: " + spaceAddress);
			registeredSpaces.Add(spaceAddress, discoveryPort);
		}

		public void UnRegister(string spaceAddress)
		{
			log.Debug("Space unregistered from discovery service: " + spaceAddress);
			registeredSpaces.Remove(spaceAddress);
		}

		public IEnumerable<ITask> ProcessDiscoveryRequest(DiscoveryRequest req)
		{
			var remoteAddress = MessageContext.Current.RemoteAddress;
			log.Debug("Received discovery request for Worker: Type=" + req.WorkerType + ", Name=" + req.WorkerName + " from " + (remoteAddress ?? "local"));
			if (LocalSpaceContainsPublicWorker(req.WorkerType, req.WorkerName))
			{
				req.ResponsePort.Post(new DiscoveryResponse {Result = DiscoveryResult.WorkerFoundRemote, WorkerAddress = space.Address});
				yield break;
			}

			var responsePort = new PortSet<DiscoveryResponse, Exception>();
			var internalDiscoveryRequest = new DiscoveryRequest
			{
				WorkerType = req.WorkerType,
				WorkerName = req.WorkerName,
				ResponsePort = responsePort
			};
			var c = new Causality("c", responsePort.P1);
			Dispatcher.AddCausality(c);
			bool found = false;
			foreach (var reg in registeredSpaces.Where(r => r.Key != remoteAddress))
			{
				reg.Value.Post(internalDiscoveryRequest);
				yield return responsePort.Choice();
				DiscoveryResponse erg = responsePort.P0;
				Exception ex = responsePort.P1;
				if (ex != null)
				{
					log.Error("Error reaching space for discovery: " + reg.Key);
					if (ex is XcoCommunicationException)
						discoveryWorker.Post(new UnRegisterSpace{SpaceAddress = reg.Key});
				}
				if (erg != null && erg.Result != DiscoveryResult.WorkerNotFound)
				{
					found = true;
					req.ResponsePort.Post(erg);
					log.Debug("Discovery request processed: Worker found at " + erg.WorkerAddress);
					break;
				}
			}
			if (!found)
			{
				req.ResponsePort.Post(new DiscoveryResponse {Result = DiscoveryResult.WorkerNotFound});
				log.Debug("Discovery request processed: Worker not found");
			}
			Dispatcher.RemoveCausality("c");
		}

		#endregion

		#region IXcoStatefulService Members

		public void Start()
		{
			if (this.IsServer)
				discoveryWorker = space.RunWorker<PDiscoveryWorker>(new DiscoveryWorker(this));
			else
				TryConnectToDiscoveryWorker();
			space.Receive(localDiscoveryPort, DiscoverLocally);
            discoveryWorkerCheckTimer = new Timer(obj => TryConnectToDiscoveryWorker(), null, discoveryWorkerCheckTimerInterval, discoveryWorkerCheckTimerInterval);
		}

		public int StartPriority
		{
			get { return 99; } //should start after all other services
		}

		public void Stop()
		{
            discoveryWorkerCheckTimer.Dispose();
			if (!this.IsServer && discoveryWorker != null)
			{
				try
				{
					discoveryWorker.Send(new UnRegisterSpace {SpaceAddress = space.Address});
				}
				catch
				{
				}
			}

		}

		#endregion

		#region IXcoService Members

		public void Initialize(IXcoServiceRegistry serviceRegistry)
		{
			space = serviceRegistry.GetSpaceInstance();
			if (space == null)
				throw new XcoConfigurationException("Could not get the local space instance from the service registry!");
		}

		#endregion

		private void TryConnectToDiscoveryWorker()
		{
			lock (this)
			{
				if (discoveryWorker == null)
				{
					try
					{
						discoveryWorker = space.ConnectWorker<PDiscoveryWorker>(this.ServerAddress);
						discoveryWorker.Post(new RegisterSpace { SpaceAddress = space.Address, DiscoveryPort = localDiscoveryPort });
					}
					catch (Exception ex)
					{
						log.Error("Could not connect to discovery worker.", ex);
					}
				}
			}
		}

		private void DiscoverLocally(DiscoveryRequest req)
		{
			var found = LocalSpaceContainsPublicWorker(req.WorkerType, req.WorkerName);
			req.ResponsePort.Post(new DiscoveryResponse
					{
						Result = found ? DiscoveryResult.WorkerFoundRemote : DiscoveryResult.WorkerNotFound,
						WorkerAddress = found ? space.Address : null
					});
			
			log.Debug("Received request to discover worker locally: WorkerType=" + req.WorkerType + (req.WorkerName != null ? (", Name=" + req.WorkerName) : "") + " -> Found: " + found);
		}

		private bool LocalSpaceContainsPublicWorker(Type workerType, string workerName)
		{
			return space.Info.Workers.Any(w => w.IsPublished && w.ContractType == workerType && w.Name == workerName);
		}
	}
}
